/*
 * Copyright (C) 2025 ttwe77
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
import java.awt.Color;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.font.FontRenderContext;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.io.File;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.List;
import java.util.Arrays;
import java.util.UUID;
import javax.imageio.ImageIO;
import javax.imageio.ImageWriter;
import javax.imageio.stream.ImageOutputStream;
import javax.imageio.IIOImage;
import javax.imageio.ImageWriteParam;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.ImageTypeSpecifier;
import org.w3c.dom.Node;
import javax.imageio.metadata.IIOMetadataNode;
import java.util.Random;
import java.util.StringJoiner;
import java.io.FileWriter;
import java.io.IOException;


public class TripleDoublePinyinKeygen_V2 {

    // QWERTY 键位顺序（26 个键）
    private static final char[] QWERTY = {
        'Q',
        'W',
        'E',
        'R',
        'T',
        'Y',
        'U',
        'I',
        'O',
        'P',
        'A',
        'S',
        'D',
        'F',
        'G',
        'H',
        'J',
        'K',
        'L',
        'Z',
        'X',
        'C',
        'V',
        'B',
        'N',
        'M'
    };

    // 小鹤双拼对应关系
    private static final String[] XIAOHE = new String[] {
        "iu", // Q
        "ei", // W
        "e", // E
        "uan&üan", // R
        "ue&üe", // T
        "un&ün", // Y
        "u", // U
        "i", // I
        "uo&o", // O
        "ie", // P
        "a", // A
        "ong&iong", // S
        "ai", // D
        ",", // F
        "eng", // G
        "ang", // H
        "an", // J
        "uai&ing", // K
        "uang&iang", // L
        "ou", // Z
        "ua&ia", // X
        "ao", // C
        "ui&ü", // V
        "in", // B
        "iao", // N
        "ian" // M
    };
    private static void printHelp() {
        System.out.println("程序使用说明");
        System.out.println("==============");
        System.out.println();
        System.out.println("用法: java Main [选项]");
        System.out.println();
        System.out.println("选项:");
        System.out.println("  -h, --help        显示此帮助信息并退出");
        System.out.println("  -o <文件名>       指定输出文件名");
        System.out.println("  -s <数值>         设置缩放比例（默认4）（整数）");
        System.out.println("  -d <数值>         设置DPI（整数）（默认600）");
        System.out.println("  -u <UUID>         设置UUID（不区分大小写）");
        System.out.println("  -r <true/false>   是否使用小鹤双拼（布尔值）");
		 System.out.println("  -j <true/false>   是否输出JSON文件（布尔值，默认false）");
        System.out.println();
        System.out.println("示例:");
        System.out.println("  java Main -o output.txt -s 100 -d 300 -u abc123");
        System.out.println("  java Main --help");
        System.out.println();
        System.out.println("注意:");
        System.out.println("  - 所有数值参数必须是有效的整数");
        System.out.println("  - UUID会自动转换为大写格式");
    }

    private static void printUsage() {
        System.out.println("用法: java Main [-o <文件名>] [-s <数值>] [-d <数值>] [-u <UUID> [-r <true/false>] [-j <true/false>]");
        System.out.println("使用 -h 或 --help 查看详细帮助");
    }
    // 在某个键盘下方绘制说明文字（三行，居中，和 UUID 保持固定间距）
    private static void drawNotes(Graphics2D g, int startX, int baseY, int keyboardWidth, int scale) {
        g.setFont(new Font("SansSerif", Font.PLAIN, 12 * scale));

        String note1 = "右上角声母 下方韵母 右下方音调 | v(eng)0或911转英文";
        String note2 = "xcgchi或110转中文 | v(u)0或xcgnum转数字 | 模式0仅声母位替换,模式1替换+列表";
        String note3 = "转英文参数<模式>[2位列][2位行][2位起始列][奇左偶右] | 转数字参数<2位字数>";

        FontMetrics fm = g.getFontMetrics();
        int textWidth1 = fm.stringWidth(note1);
        int textWidth2 = fm.stringWidth(note2);
        int textWidth3 = fm.stringWidth(note3);

        int centerX1 = startX + (keyboardWidth - textWidth1) / 2;
        int centerX2 = startX + (keyboardWidth - textWidth2) / 2;
        int centerX3 = startX + (keyboardWidth - textWidth3) / 2;

        // 固定间距
        int posY1 = baseY + 30 * scale;
        int posY2 = baseY + 45 * scale;
        int posY3 = baseY + 60 * scale;

        g.drawString(note1, centerX1, posY1);
        g.drawString(note2, centerX2, posY2);
        g.drawString(note3, centerX3, posY3);
    }


    public static void main(String[] args) throws Exception {
        // 生成一个 UUID（全部大写），在两个键盘下保持一致
        String uuid = UUID.randomUUID().toString().toUpperCase();
        String outFile = uuid + ".png";
        int scale = 4;
        int dpi = 600;
        boolean useXiaoheShuangpin = false; // 默认不使用小鹤双拼
		 boolean outputJson = false; // 默认不输出 JSON

        // 遍历 args
        for (int i = 0; i < args.length; i++) {
            switch (args[i]) {
                case "--help":
                case "-h":
                    printHelp();
                    System.exit(0);
                    break;
                case "-o":
                    if (i + 1 < args.length) outFile = args[++i];
                    break;
                case "-s":
                    if (i + 1 < args.length) scale = Integer.parseInt(args[++i]);
                    break;
                case "-d":
                    if (i + 1 < args.length) dpi = Integer.parseInt(args[++i]);
                    break;
                case "-u":
                    if (i + 1 < args.length) uuid = args[++i].toUpperCase();
                    outFile = uuid + ".png";
                    break;
                case "-r":
                    if (i + 1 < args.length) {
                        String value = args[++i].toLowerCase();
                        if (value.equals("true") || value.equals("1") || value.equals("yes")) {
                            useXiaoheShuangpin = true;
                        } else if (value.equals("false") || value.equals("0") || value.equals("no")) {
                            useXiaoheShuangpin = false;
                        } else {
                            System.err.println("错误: -r 参数值必须是 true/false, 1/0, 或 yes/no");
                            System.exit(1);
                        }
                    }
                    break;
					 case "-j":
                if (i + 1 < args.length) {
                    String value = args[++i].toLowerCase();
                    if (value.equals("true") || value.equals("1") || value.equals("yes")) {
                        outputJson = true;
                    } else if (value.equals("false") || value.equals("0") || value.equals("no")) {
                        outputJson = false;
                    } else {
                        System.err.println("错误: -j 参数值必须是 true/false, 1/0, 或 yes/no");
                        System.exit(1);
                    }
                }
                break;
                default:
                    System.err.println("未知参数: " + args[i]);
                    printUsage();
                    System.exit(1);
            }
        }

        // 用 UUID 生成种子
        UUID u = UUID.fromString(uuid);
        // 把 UUID 拆成两个 long
        long seed = u.getMostSignificantBits() ^ u.getLeastSignificantBits();
        // 用这个 seed 初始化 Random
        Random rnd = new Random(seed);
        // 原始逻辑尺寸（易于调整）——将会乘以 scale
        int baseWidth = 830;
        int baseHeight = 790; // 给上下两个键盘留出空间
        int width = baseWidth * scale;
        int height = baseHeight * scale;

        BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
        Graphics2D g = img.createGraphics();

        // 更全面的高质量渲染设置（避免模糊）
        g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_HRGB);
        g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
        g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
        g.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
        g.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE);
        g.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY);
        g.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);

        // 背景
        g.setColor(Color.WHITE);
        g.fillRect(0, 0, width, height);
        // 随机分配表1（A..Z -> a..z，不重复）
        char[] lowerAlpha = new char[26];
        for (int i = 0; i < 26; i++) lowerAlpha[i] = (char)('a' + i);
        shuffleArray(lowerAlpha, rnd);

        // upper A..Z
        char[] upperAlpha = new char[26];
        for (int i = 0; i < 26; i++) upperAlpha[i] = (char)('A' + i);

        // p 映射（表1）: Upper -> lower
        Map < Character, Character > p = new HashMap < > ();
        for (int i = 0; i < 26; i++) p.put(upperAlpha[i], lowerAlpha[i]);

        // inv 映射（表2）构造：保证如果表1中 A 的右上是 g，则表2 中 G 的右上是 a
        Map < Character, Character > inv = new HashMap < > ();
        for (int i = 0; i < 26; i++) {
            char U = upperAlpha[i];
            char uLower = Character.toLowerCase(U);
            int pos = -1;
            for (int j = 0; j < 26; j++)
                if (lowerAlpha[j] == uLower) {
                    pos = j;
                    break;
                }
            if (pos == -1) inv.put(U, '?');
            else inv.put(U, Character.toLowerCase(upperAlpha[pos]));
        }

        // 音调 1..4，按键位（qwerty 索引）随机生成并在两个键盘保持一致
        // 分布生成 26 个音调：1..4 各 5 个，5..9 与 0 各 1 个，使用同一随机种子打乱
        List < Integer > toneList = new ArrayList < > (26);
        // 1..4 各 5 个
        for (int t = 1; t <= 4; t++) {
            for (int k = 0; k < 5; k++) {
                toneList.add(t);
            }
        }
        // 5..9 各 1 个
        for (int t = 5; t <= 9; t++) toneList.add(t);
        // 0 一个（作为单独的一种音调字符 '0'）
        toneList.add(0);

        // 使用与其它随机操作相同的 rnd 打乱，保证可复现
        Collections.shuffle(toneList, rnd);

        // 填回数组，按 qwerty 索引一一对应
        int[] tones = new int[26];
        for (int i = 0; i < 26; i++) tones[i] = toneList.get(i);

        // 双拼列表
        List < String > doublePinyin = new ArrayList < > (Arrays.asList(XIAOHE));
        if (!useXiaoheShuangpin) {
            Collections.shuffle(doublePinyin, rnd);
        }

        // 布局参数（单个键盘） — 基础尺寸 * scale
        int keyW = 68 * scale;
        int keyH = 68 * scale;
        int hGap = 8 * scale;
        int vGap = 26 * scale;
        int marginX = 60 * scale;
        int startX = marginX;

        // 字体设置（基于 scale 放大）
        Font upperFont = new Font("Consolas", Font.BOLD, 40 * scale);
        Font smallUR = new Font("Consolas", Font.PLAIN, 14 * scale);
        Font smallLR = new Font("Consolas", Font.PLAIN, 16 * scale);
        Font pinyinFont = new Font("Consolas", Font.PLAIN, 14 * scale);
        Font titleFont = new Font("SansSerif", Font.PLAIN, 18 * scale);
        Font uuidFont = new Font("SansSerif", Font.BOLD, 16 * scale);

        // 标题
        g.setColor(Color.BLACK);
        g.setFont(titleFont);
        g.drawString("三重双拼混合加密方案 对称密钥（上表：映射 p；下表：逆映射 p^{-1}）", startX, 26 * scale);

        // 先绘制上表（表1），位于 topStartY
        int topStartY = 50 * scale;
        drawKeyboardGeneric(g, startX, topStartY, keyW, keyH, hGap, vGap,
            QWERTY, upperFont, smallUR, smallLR, pinyinFont,
            (upperChar, posIndex) -> p.getOrDefault(upperChar, '?'),
            (upperChar, posIndex) -> tones[posIndex],
            doublePinyin);

        // 计算键盘实际占宽（以第一行为准）：10 keys + 9 gaps
        int keyboardWidth = keyW * 10 + hGap * 9;
        // top UUID 位置：在上键盘下方居中显示
        int topUuidY = topStartY + (keyH * 3 + vGap * 3) + (12 * scale);
        drawCenteredString(g, uuid, startX, topUuidY, keyboardWidth, uuidFont);
        // 在上表下方绘制说明文字
        drawNotes(g, startX, topUuidY, keyboardWidth, scale);

        // 绘制下表（表2），位于下方（加上一个间隔）
        int interKeyboardGap = 75 * scale;
        int bottomStartY = topStartY + (keyH * 3 + vGap * 2) + interKeyboardGap + (50 * scale); // 留出上面 UUID 区域
        drawKeyboardGeneric(g, startX, bottomStartY, keyW, keyH, hGap, vGap,
            QWERTY, upperFont, smallUR, smallLR, pinyinFont,
            (upperChar, posIndex) -> inv.getOrDefault(upperChar, '?'),
            (upperChar, posIndex) -> tones[posIndex],
            doublePinyin);

        // bottom UUID 位置：在下键盘下方居中显示（同一个 UUID）
        int bottomUuidY = bottomStartY + (keyH * 3 + vGap * 3) + (12 * scale);
        drawCenteredString(g, uuid, startX, bottomUuidY, keyboardWidth, uuidFont);

        // 在下表下方绘制说明文字
        drawNotes(g, startX, bottomUuidY, keyboardWidth, scale);
        g.dispose();

                // 写出 PNG（含 DPI）
        writePngWithDPI(img, new File(outFile), dpi);
		        System.out.println("生成完成：" + outFile + " (scale=" + scale + ", dpi=" + dpi + ")");
        System.out.println("UUID: " + uuid);
          // 写出同名 JSON（包含映射 p, inv, pinyin, tones）
    if (outputJson) {
        try {
            String jsonName = outFile;
            if (jsonName.toLowerCase().endsWith(".png")) {
                jsonName = jsonName.substring(0, jsonName.length() - 4) + ".json";
            } else {
                jsonName = jsonName + ".json";
            }
            File jsonFile = new File(jsonName);
            writeJsonMapping(jsonFile, p, inv, doublePinyin, tones, uuid);
            System.out.println("同时写出 JSON: " + jsonName);
        } catch (Exception e) {
            System.err.println("警告：写出 JSON 失败 - " + e.getMessage());
            e.printStackTrace();
        }
    }
    }

    // 通用绘制单个键盘（按 QWERTY 排列 3 行）
    private static void drawKeyboardGeneric(Graphics2D g, int startX, int startY,
        int keyW, int keyH, int hGap, int vGap,
        char[] qwerty, Font upperFont, Font smallUR, Font smallLR, Font pinyinFont,
        CharProvider topRightProvider, IntProvider toneProvider,
        List < String > pinyinList) {
        g.setColor(Color.DARK_GRAY);

        int index = 0;
        int y = startY;
        int x = startX;
        // 第一行 10 键
        for (int col = 0; col < 10; col++) {
            char U = qwerty[index];
            char tr = topRightProvider.get(U, index);
            int t = toneProvider.get(U, index);
            drawKey(g, x, y, keyW, keyH, U, tr, t, pinyinList.get(index), upperFont, smallUR, smallLR, pinyinFont);
            index++;
            x += keyW + hGap;
        }
        // 第二行（缩进 half key）
        y += keyH + vGap;
        x = startX + (keyW / 2);
        for (int col = 0; col < 9; col++) {
            char U = qwerty[index];
            char tr = topRightProvider.get(U, index);
            int t = toneProvider.get(U, index);
            drawKey(g, x, y, keyW, keyH, U, tr, t, pinyinList.get(index), upperFont, smallUR, smallLR, pinyinFont);
            index++;
            x += keyW + hGap;
        }
        // 第三行（更大缩进）
        y += keyH + vGap;
        x = startX + keyW;
        for (int col = 0; col < 7; col++) {
            char U = qwerty[index];
            char tr = topRightProvider.get(U, index);
            int t = toneProvider.get(U, index);
            drawKey(g, x, y, keyW, keyH, U, tr, t, pinyinList.get(index), upperFont, smallUR, smallLR, pinyinFont);
            index++;
            x += keyW + hGap;
        }
    }

    private static void drawKey(Graphics2D g, int x, int y, int w, int h,
        char U, char topRightChar, int tone, String pinyin,
        Font upperFont, Font smallUR, Font smallLR, Font pinyinFont) {
        // 背景与边框
        g.setColor(new Color(245, 245, 245));
        g.fillRoundRect(x, y, w, h, Math.max(8, w / 16), Math.max(8, h / 16));
        g.setColor(Color.GRAY);
        g.drawRoundRect(x, y, w, h, Math.max(8, w / 16), Math.max(8, h / 16));

        // 大写字母居中
        g.setFont(upperFont);
        FontMetrics fm = g.getFontMetrics();
        Rectangle2D ub = fm.getStringBounds(String.valueOf(U), g);
        int ux = x + (w - (int) ub.getWidth()) / 2;
        int uy = y + (h / 2) + fm.getAscent() / 2 - (int)(0.15 * fm.getAscent());
        g.setColor(Color.BLACK);
        g.drawString(String.valueOf(U), ux, uy);

        // 右上小写字母（声母）
        if (topRightChar == 0) topRightChar = '?';
        g.setFont(smallUR);
        String tur = String.valueOf(topRightChar);
        FontMetrics fmUR = g.getFontMetrics();
        int urX = x + w - fmUR.stringWidth(tur) - (6 * (w / 68));
        int urY = y + fmUR.getAscent() + (6 * (h / 68));
        g.drawString(tur, urX, urY);

        // 右下音调数字（对齐键底部）
        g.setFont(smallLR);
        String toneS = String.valueOf(tone);
        FontMetrics fmLR = g.getFontMetrics();
        int lrX = x + w - fmLR.stringWidth(toneS) - (6 * (w / 68));
        int lrY = y + h - (6 * (h / 68));
        g.drawString(toneS, lrX, lrY);

        // 下方双拼（最多两行）
        g.setFont(pinyinFont);
        FontMetrics fmp = g.getFontMetrics();
        String[] parts = pinyin.split("\n");
        int lines = Math.min(2, parts.length);
        for (int i = 0; i < lines; i++) {
            String line = parts[i];
            int px = x + (w - fmp.stringWidth(line)) / 2;
            int py = y + h + (16 * (h / 68)) + i * (fmp.getHeight());
            g.drawString(line, px, py);
        }
    }

    // 居中绘制字符串（在给定的矩形区域内水平居中）
    private static void drawCenteredString(Graphics2D g, String text, int rectX, int rectY, int rectWidth, Font font) {
        g.setFont(font);
        FontMetrics fm = g.getFontMetrics();
        int textWidth = fm.stringWidth(text);
        int x = rectX + (rectWidth - textWidth) / 2;
        int y = rectY + fm.getAscent(); // rectY 给的是 baseline 上方的偏移量，这里直接用 baseline 风格
        g.setColor(Color.DARK_GRAY);
        g.drawString(text, x, y);
    }

    private static void shuffleArray(char[] arr, Random rnd) {
        for (int i = arr.length - 1; i > 0; i--) {
            int j = rnd.nextInt(i + 1);
            char t = arr[i];
            arr[i] = arr[j];
            arr[j] = t;
        }
    }

    // provider 接口
    private interface CharProvider {
        char get(char upperChar, int posIndex);
    }

    private interface IntProvider {
        int get(char upperChar, int posIndex);
    }

    // 写出 PNG 并设置 pHYs (pixels per unit — 用于指定 DPI)
    private static void writePngWithDPI(BufferedImage image, File outFile, int dpi) throws Exception {
        // 计算 pixels per meter (pHYs 单位是每米像素数)
        int pixelsPerMeter = (int) Math.round(dpi * 39.3700787); // dpi / 0.0254

        ImageWriter writer = ImageIO.getImageWritersByFormatName("png").next();
        ImageWriteParam writeParam = writer.getDefaultWriteParam();
        ImageTypeSpecifier typeSpecifier = ImageTypeSpecifier.createFromBufferedImageType(image.getType());
        IIOMetadata metadata = writer.getDefaultImageMetadata(typeSpecifier, writeParam);

        // 构造 PNG 元数据节点树
        String nativeFormat = "javax_imageio_png_1.0";
        IIOMetadataNode root = new IIOMetadataNode(nativeFormat);

        IIOMetadataNode pHYs_node = new IIOMetadataNode("pHYs");
        pHYs_node.setAttribute("pixelsPerUnitXAxis", Integer.toString(pixelsPerMeter));
        pHYs_node.setAttribute("pixelsPerUnitYAxis", Integer.toString(pixelsPerMeter));
        pHYs_node.setAttribute("unitSpecifier", "meter");
        root.appendChild(pHYs_node);

        try {
            metadata.mergeTree(nativeFormat, root);
        } catch (Exception e) {
            // 如果合并失败，不要中断写入；仍然尝试直接写文件
            System.err.println("警告：无法写入 PNG DPI 元数据 - " + e.getMessage());
        }

        try (ImageOutputStream stream = ImageIO.createImageOutputStream(outFile)) {
            writer.setOutput(stream);
            IIOImage iioImage = new IIOImage(image, null, metadata);
            writer.write(null, iioImage, writeParam);
            writer.dispose();
        }
    }
	// 在 writePngWithDPI 方法之后粘贴以下方法（确保放在类内部，但在最后的类结束花括号之前）

private static void writeJsonMapping(File jsonFile,
                                     Map<Character, Character> p,
                                     Map<Character, Character> inv,
                                     List<String> pinyinList,
                                     int[] tones,
                                     String uuid) throws IOException {
    StringBuilder sb = new StringBuilder();
    sb.append("{\n");
    sb.append("  \"uuid\": \"").append(escapeJsonString(uuid)).append("\",\n");

    // mapping_p
    sb.append("  \"mapping_p\": {\n");
    for (char c = 'A'; c <= 'Z'; c++) {
        char v = p.getOrDefault(c, '?');
        sb.append("    \"").append(c).append("\": \"").append(escapeJsonString(String.valueOf(v))).append("\"");
        if (c != 'Z') sb.append(",");
        sb.append("\n");
    }
    sb.append("  },\n");

    // mapping_inv
    sb.append("  \"mapping_inv\": {\n");
    for (char c = 'A'; c <= 'Z'; c++) {
        char v = inv.getOrDefault(c, '?');
        sb.append("    \"").append(c).append("\": \"").append(escapeJsonString(String.valueOf(v))).append("\"");
        if (c != 'Z') sb.append(",");
        sb.append("\n");
    }
    sb.append("  },\n");

    // mapping_pinyin (按 QWERTY 键位顺序)
    sb.append("  \"mapping_pinyin\": {\n");
    for (char c = 'A'; c <= 'Z'; c++) {
        int pos = findQwertyIndex(c);
        String py = "";
        if (pos >= 0 && pos < pinyinList.size()) {
            py = pinyinList.get(pos);
        }
        sb.append("    \"").append(c).append("\": \"").append(escapeJsonString(py)).append("\"");
        if (c != 'Z') sb.append(",");
        sb.append("\n");
    }
    sb.append("  },\n");

    // mapping_tone
    sb.append("  \"mapping_tone\": {\n");
    for (char c = 'A'; c <= 'Z'; c++) {
        int pos = findQwertyIndex(c);
        int t = -1;
        if (pos >= 0 && pos < tones.length) t = tones[pos];
        sb.append("    \"").append(c).append("\": ").append(t);
        if (c != 'Z') sb.append(",");
        sb.append("\n");
    }
    sb.append("  }\n");

    sb.append("}\n");

    try (FileWriter fw = new FileWriter(jsonFile)) {
        fw.write(sb.toString());
        fw.flush();
    }
}

private static int findQwertyIndex(char upper) {
    for (int i = 0; i < QWERTY.length; i++) {
        if (QWERTY[i] == upper) return i;
    }
    return -1;
}

private static String escapeJsonString(String s) {
    if (s == null) return "";
    StringBuilder out = new StringBuilder();
    for (int i = 0; i < s.length(); i++) {
        char ch = s.charAt(i);
        switch (ch) {
            case '\\': out.append("\\\\"); break;
            case '"': out.append("\\\""); break;
            case '\b': out.append("\\b"); break;
            case '\f': out.append("\\f"); break;
            case '\n': out.append("\\n"); break;
            case '\r': out.append("\\r"); break;
            case '\t': out.append("\\t"); break;
            default:
                if (ch < 0x20 || ch > 0x7E) {
                    out.append(String.format("\\u%04x", (int) ch));
                } else {
                    out.append(ch);
                }
        }
    }
    return out.toString();
}

}